﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xrm.Client.Services;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace CrmRulo.v2011
{
    /// <summary>
    /// Uploads a set of rdl files to a Dynamics CRM deployment.
    /// </summary>
    public class Crm2011ReportsUploader
    {
        private readonly OrganizationService _crm;

        /// <summary>
        /// Class constructor
        /// </summary>
        /// <param name="crm">Connection to CRM Organization Service</param>
        public Crm2011ReportsUploader(OrganizationService crm)
        {
            _crm = crm;
        }

        /// <summary>
        /// Uploads all the *.rdl files found in directory to CRM
        /// </summary>
        /// <param name="directoryPath">Path to directory</param>
        public void UploadAllReports(string directoryPath)
        {
            var configFilePath = directoryPath + @"\publishconfig.xml";
            if (!File.Exists(configFilePath))
            {
                CreateConfigFile(configFilePath);
                Console.WriteLine("Config file has been created. Please review before publishing.");
                return;
            }
            var config = XElement.Load(configFilePath);

            string publishDirectory = ".";

            var publishDirNode = config.Attribute("directory");
            if (publishDirNode != null)
                publishDirectory = publishDirNode.Value;

            foreach (var report in config.Elements("report"))
            {
                var att = report.Attribute("filename");
                Util.Assert(att != null, "Attribute 'filename' not found in report.");
                var reportFileName = String.Format("{0}\\{1}", publishDirectory, att.Value);
                if (!File.Exists(reportFileName))
                {
                    Console.WriteLine("ERROR: Report file '{0}' not found. Skipping.", reportFileName);
                    continue;
                }

                var visibilityCodes = report.Elements("visibility").Select(r => (int)Enum.Parse(typeof(ReportVisibilityCode), r.Value)).ToArray();
                var categoryCodes = report.Elements("category").Select(r => (int)Enum.Parse(typeof(ReportCategoryCode), r.Value)).ToArray();
                var entityLogicalNames = report.Elements("entity").Select(r => r.Value).ToArray();

                att = report.Attribute("name");
                Util.Assert(att != null, "Attribute 'name' not found in report.");
                var reportName = att.Value;

                string reportDescription = String.Empty;
                att = report.Attribute("description");
                if (att != null)
                    reportDescription = att.Value;

                var languageCode = -1; // All languages by default
                att = report.Attribute("languagecode");
                if (att != null)
                    languageCode = Int32.Parse(att.Value);

                string parentReportId = null;
                att = report.Attribute("parent");
                if (att != null)
                    parentReportId = FindParentReport(att.Value);

                var reportObject = new Report
                                       {
                                           PathToReportFile = reportFileName,
                                           Name = reportName,
                                           EntityLogicalNames = entityLogicalNames,
                                           VisibilityCodes = visibilityCodes,
                                           CategoryCodes = categoryCodes,
                                           ParentReportName = parentReportId,
                                           LanguageCode = languageCode,
                                           Description = reportDescription
                                       };
                UploadReport(reportObject);
                //UploadReport(reportFileName, reportName, entityLogicalNames, visibilityCodes, categoryCodes, parentReportId, languageCode, reportDescription);
            }
        }

        /// <summary>
        /// Searches for a report in CRM. Fails if more than one report with the given name exist.
        /// </summary>
        /// <param name="reportName">The name of the report.</param>
        /// <returns>The id of the report, null if not found.</returns>
        private string FindReport(string reportName)
        {
            var q = new QueryByAttribute("report");
            q.AddAttributeValue("name", reportName);
            q.ColumnSet = new ColumnSet(false);
            var res = _crm.RetrieveMultiple(q);
            if (res.Entities.Count == 0)
                return null;
            if (res.Entities.Count > 1)
                throw new Exception("More than one report found with name: " + reportName);
            return res[0].Id.ToString();
        }

        /// <summary>
        /// Searches for an existing report with the given name
        /// </summary>
        /// <param name="reportName">The name of the report</param>
        /// <returns>The id of the report. Fails if not found or duplicate.</returns>
        private string FindParentReport(string reportName)
        {
            try
            {
                var parentReportId = FindReport(reportName);
                if (parentReportId == null)
                    throw new Exception("Parent report not found: " + reportName);
                return parentReportId;
            }
            catch (Exception ex)
            {
                throw new Exception("Error retrieving parent report.", ex);
            }
        }

        /// <summary>
        /// Creates a boilerplate configuration file for the reports found in the specified directory.
        /// </summary>
        /// <param name="configFilePath">Path to resulting configuration file to be created</param>
        public void CreateConfigFile(string configFilePath)
        {

            var directoryPath = Path.GetDirectoryName(configFilePath);
            var config = new XElement("publish", new XAttribute("directory", directoryPath));
            var reportFiles = Directory.GetFiles(directoryPath).Where(f => f.EndsWith(".rdl"));

            foreach (var file in reportFiles)
            {
                var fileName = new FileInfo(file).Name;
                config.Add(new XElement("report",
                    new XAttribute("filename", fileName),
                    new XAttribute("name", fileName),
                    new XAttribute("description", fileName),
                    new XAttribute("parent", "PARENTREPORT"),
                    new XAttribute("languagecode", "-1"),

                        new XElement("entity", "ENTITYLOGICALNAME_1"),
                        new XElement("entity", "ENTITYLOGICALNAME_2"),

                        new XElement("visibility", ReportVisibilityCode.Form),
                        new XElement("visibility", ReportVisibilityCode.Grid),
                        new XElement("visibility", ReportVisibilityCode.ReportsGrid),

                        new XElement("category", ReportCategoryCode.SalesReports),
                        new XElement("category", ReportCategoryCode.ServiceReports),
                        new XElement("category", ReportCategoryCode.MarketingReports),
                        new XElement("category", ReportCategoryCode.AdministrativeReports)
                    ));
            }
            config.Save(configFilePath);
        }


        private void UploadReport(Report r)
        {
            string reportText = File.ReadAllText(r.PathToReportFile);
            string reportFileName = Path.GetFileName(r.PathToReportFile);
            var report = new Entity("report");
            report["name"] = r.Name;
            report["filename"] = reportFileName;
            report["bodytext"] = reportText;
            report["reporttypecode"] = new OptionSetValue(1); // Reporting services
            report["languagecode"] = r.LanguageCode;
            report["ispersonal"] = false;
            report["description"] = r.Description;

            if (!String.IsNullOrEmpty(r.ParentReportName))
                report["parentreportid"] = new EntityReference("report", new Guid(r.ParentReportName));

            Guid reportId;
            var existingReportId = FindReport(r.Name);
            if (existingReportId == null)
            {
                reportId = _crm.Create(report);
                Console.WriteLine("Creating report '{0}'", r.Name);
            }
            else
            {
                reportId = new Guid(existingReportId);
                report.Id = reportId;
                RemoveCategoryInformation(reportId);
                RemoveVisibilityInformation(reportId);
                RemoveEntityCodeInformation(reportId);
                _crm.Update(report);
                Console.WriteLine("Updating report '{0}'", r.Name);
            }

            foreach (var visibility in r.VisibilityCodes)
                AddVisibility(reportId, visibility);
            foreach (var category in r.CategoryCodes)
                AddCategory(reportId, category);
            foreach (var entityLogicalName in r.EntityLogicalNames)
                AddEntityCode(reportId, entityLogicalName);
        }

        /// <summary>
        /// Uploads or updates a SQL Server Reporting Services report file to CRM
        /// </summary>
        /// <param name="pathToReport">Report file name</param>
        /// <param name="reportName">Name of report in CRM</param>
        /// <param name="entityLogicalNames">List of entities associated with this report</param>
        /// <param name="visibilityCodes">List of visibility codes</param>
        /// <param name="categoryCodes">List of category codes</param>
        /// <param name="parentReportId">Optional: id of parent record in CRM</param>
        /// <param name="languageCode">Language code to apply to report. -1 for 'all languages'</param>
        /// <param name="reportDescription">Descriptive text for the report</param>
        private void UploadReport(string pathToReport, string reportName, IEnumerable<string> entityLogicalNames, IEnumerable<int> visibilityCodes, IEnumerable<int> categoryCodes, string parentReportId, int languageCode, string reportDescription)
        {
            string reportText = File.ReadAllText(pathToReport);
            string reportFileName = Path.GetFileName(pathToReport);
            var report = new Entity("report");
            report["name"] = reportName;
            report["filename"] = reportFileName;
            report["bodytext"] = reportText;
            report["reporttypecode"] = new OptionSetValue(1); // Reporting services
            report["languagecode"] = languageCode;
            report["ispersonal"] = false;
            report["description"] = reportDescription;

            if (!String.IsNullOrEmpty(parentReportId))
                report["parentreportid"] = new EntityReference("report", new Guid(parentReportId));

            Guid reportId;
            var existingReportId = FindReport(reportName);
            if (existingReportId == null)
            {
                reportId = _crm.Create(report);
                Console.WriteLine("Creating report '{0}'", reportName);
            }
            else
            {
                reportId = new Guid(existingReportId);
                report.Id = reportId;
                RemoveCategoryInformation(reportId);
                RemoveVisibilityInformation(reportId);
                RemoveEntityCodeInformation(reportId);
                _crm.Update(report);
                Console.WriteLine("Updating report '{0}'", reportName);
            }

            foreach (var visibility in visibilityCodes)
                AddVisibility(reportId, visibility);
            foreach (var category in categoryCodes)
                AddCategory(reportId, category);
            foreach (var entityLogicalName in entityLogicalNames)
                AddEntityCode(reportId, entityLogicalName);
        }

        /// <summary>
        /// Creates an auxiliary reportentity record for a report
        /// </summary>
        /// <param name="reportId">The report id</param>
        /// <param name="entityLogicalName">The entity name to attach the report to</param>
        private void AddEntityCode(Guid reportId, string entityLogicalName)
        {
            var reportEntity = new Entity("reportentity");
            reportEntity["reportid"] = new EntityReference("report", reportId);
            reportEntity["objecttypecode"] = entityLogicalName;
            _crm.Create(reportEntity);
        }

        /// <summary>
        /// Deletes all auxiliary reportentity records for a report
        /// </summary>
        /// <param name="reportId">The report id</param>
        private void RemoveEntityCodeInformation(Guid reportId)
        {
            var q = new QueryByAttribute("reportentity");
            q.AddAttributeValue("reportid", reportId);
            var res = _crm.RetrieveMultiple(q);
            foreach (var record in res.Entities)
                _crm.Delete("reportentity", record.Id);
        }

        /// <summary>
        /// Creates an auxiliary reportcategory record for a report
        /// </summary>
        /// <param name="reportId">The report id</param>
        /// <param name="category">The category id  (see ReportCategory enum)</param>
        private void AddCategory(Guid reportId, int category)
        {
            var reportCategory = new Entity("reportcategory");
            reportCategory["reportid"] = new EntityReference("report", reportId);
            reportCategory["categorycode"] = new OptionSetValue(category);
            _crm.Create(reportCategory);
        }

        /// <summary>
        /// Deletes all auxiliary reportcategory records for a report
        /// </summary>
        /// <param name="reportId">The report id</param>
        private void RemoveCategoryInformation(Guid reportId)
        {
            var q = new QueryByAttribute("reportcategory");
            q.AddAttributeValue("reportid", reportId);
            var res = _crm.RetrieveMultiple(q);
            foreach (var record in res.Entities)
                _crm.Delete("reportcategory", record.Id);
        }

        /// <summary>
        /// Creates an auxiliary reportvisibility record for a report
        /// </summary>
        /// <param name="reportId">The report id</param>
        /// <param name="visibility">The visility value identificator (see ReportVisibilityCode enum)</param>
        private void AddVisibility(Guid reportId, int visibility)
        {
            var reportVisibility = new Entity("reportvisibility");
            reportVisibility["reportid"] = new EntityReference("report", reportId);
            reportVisibility["visibilitycode"] = new OptionSetValue(visibility);
            _crm.Create(reportVisibility);
        }

        /// <summary>
        /// Deletes all auxiliary reportvisibility records for a report
        /// </summary>
        /// <param name="reportId">The report id</param>
        private void RemoveVisibilityInformation(Guid reportId)
        {
            var q = new QueryByAttribute("reportvisibility");
            q.AddAttributeValue("reportid", reportId);
            var res = _crm.RetrieveMultiple(q);
            foreach (var record in res.Entities)
                _crm.Delete("reportvisibility", record.Id);
        }
    }
}
